import { JKManager } from '../JKManager'
import Md5ByteArrayTool from './Md5ByteArrayTool'

/**
 * 热更新工具单例
 */
export class HotUpdate {
    private _outEventCall: (info: string, event: jsb.EventAssetsManager) => void
    private _updating: boolean
    private _canRetry: boolean
    private _storagePath: string
    private _assetsManager: jsb.AssetsManager
    private _checkListener
    private _updateListener
    private _failCount: number

    /**
     * 初始化，热更前调用，热更后调用clear进行清理
     * @param outEventCall 输出热更状态回调
     */
    init(outEventCall: (info: string, event: jsb.EventAssetsManager) => void): void {
        cc.log(`${cc.js.getClassName(this)} init`)

        this._outEventCall = outEventCall
        this._updating = false
        this._canRetry = false
        this._storagePath = ``

        // Hot update is only available in Native build
        if (cc.sys.isNative && !cc.sys.isBrowser) {
            this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : `/`) + `remote-asset`)
            cc.log(`${cc.js.getClassName(this)} Storage path for remote asset : ${this._storagePath}`)

            // Init with empty manifest url for testing custom manifest
            this._assetsManager = new jsb.AssetsManager(``, this._storagePath, (versionA, versionB) => {
                // Setup your own version compare handler, versionA and B is versions in string
                // if the return value greater than 0, versionA is greater than B,
                // if the return value equals 0, versionA equals to B,
                // if the return value smaller than 0, versionA is smaller than B.
                cc.log(`${cc.js.getClassName(this)} JS Custom Version Compare: version A is ${versionA}, version B is ${versionB}`)

                JKManager.LocalStorage.setData(JKManager.LocalStorage.LocalStorageKey.AppVersion, versionA)
                JKManager.AppConfig.AppVersion = versionA
                let vA = versionA.split(`.`),
                    vB = versionB.split(`.`)
                if (vB.length === vA.length) {
                    for (let i = 0; i < vA.length; ++i) {
                        let a = parseInt(vA[i]),
                            b = parseInt(vB[i] || `0`)
                        if (a === b) {
                            continue
                        } else {
                            return a - b
                        }
                    }
                }
                if (vB.length > vA.length) {
                    return -1
                } else {
                    return 0
                }
            })

            // let panel = this.panel
            // Setup the verification callback, but we don`t have md5 check function yet, so only print some message
            // Return true if the verification passed, otherwise return false
            this._assetsManager.setVerifyCallback((path, asset) => {
                // When asset is compressed, we don`t need to check its md5, because zip file have been deleted.
                let compressed = asset.compressed
                // Retrieve the correct md5 value.
                let expectedMD5 = asset.md5
                // asset.path is relative path and path is absolute.
                // let relativePath = asset.path
                // The size of asset file, but this value could be absent.
                // let size = asset.size

                return this._verifyMd5(path, expectedMD5, compressed)
            })

            cc.log(`${cc.js.getClassName(this)} Hot update is ready, please check or directly update.`)

            if (cc.sys.os === cc.sys.OS_ANDROID) {
                // Some Android device may slow down the download process when concurrent tasks is too much.
                // The value may not be accurate, please do more test and find what`s most suitable for your game.
                // this._assetsManager.setMaxConcurrentTask(2);
                cc.log(`${cc.js.getClassName(this)} Max concurrent tasks count have been limited to 2`)
            }
        }
    }

    //==============================================================================================================================
    //========================================================= 内部方法 ============================================================
    //==============================================================================================================================

    /**
     * md5校验
     */
    _verifyMd5(path: string, md5: string, compressed: boolean): boolean {
        cc.log(`${cc.js.getClassName(this)} _verifyMd5`)

        let data = (jsb.fileUtils as any).getDataFromFile(path)
        if (data) {
            if (!compressed && md5) {
                let md5Vaule = Md5ByteArrayTool(data)
                if (md5 != md5Vaule) {
                    cc.log(`${cc.js.getClassName(this)} asset verify md5 failed! path = ${path} md5 = ${md5} fail = ${path}`)

                    return false
                }
            }
        } else {
            cc.log(`${cc.js.getClassName(this)} asset verify not find file! path = ${path}`)

            return false
        }

        return true
    }

    /**
     * 热更事件回调
     * @param event 
     */
    _onEventCall(event: jsb.EventAssetsManager): void {
        cc.log(`${cc.js.getClassName(this)} _onEventCall Code: ${event.getEventCode()}`)

        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                // cc.log(`${cc.js.getClassName(this)} No local manifest file found, hot update skipped.`)
                this._outEventCall && this._outEventCall(`No local manifest file found, hot update skipped.`, event)

                break
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                // cc.log(`${cc.js.getClassName(this)} Fail to download manifest file, hot update skipped.`)
                this._outEventCall && this._outEventCall(`Fail to download manifest file, hot update skipped.`, event)

                break
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                // cc.log(`${cc.js.getClassName(this)} Already up to date with the latest remote version.`)
                this._outEventCall && this._outEventCall(`Already up to date with the latest remote version.`, event)

                break
            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                // cc.log(`${cc.js.getClassName(this)} New version found, please try to update. (${this._assetsManager.getTotalBytes()})`)
                this._outEventCall && this._outEventCall(`New version found, please try to update. (${this._assetsManager.getTotalBytes()})`, event)

                break

            default:
                return
        }

        this._assetsManager.setEventCallback(null)
        this._checkListener = null
        this._updating = false
    }

    /**
     * 更新中回调
     * @param event
     */
    _onUpdateCall(event: jsb.EventAssetsManager): void {
        cc.log(`${cc.js.getClassName(this)} _onUpdateCall Code: ${event.getEventCode()}`)

        let needRestart = false,
            failed = false
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                // cc.log(`${cc.js.getClassName(this)} No local manifest file found, hot update skipped.`)
                this._outEventCall && this._outEventCall(`No local manifest file found, hot update skipped.`, event)
                failed = true

                break
            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                let msg = event.getMessage()
                this._outEventCall && this._outEventCall(msg || ``, event)

                break
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                // cc.log(`${cc.js.getClassName(this)} Fail to download manifest file, hot update skipped.`)

                failed = true
                this._outEventCall && this._outEventCall(`Fail to download manifest file, hot update skipped.`, event)

                break
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                // cc.log(`${cc.js.getClassName(this)} Already up to date with the latest remote version.`)

                failed = true
                this._outEventCall && this._outEventCall(`Already up to date with the latest remote version.`, event)

                break
            case jsb.EventAssetsManager.UPDATE_FINISHED:
                // cc.log(`${cc.js.getClassName(this)} Update finished. ${event.getMessage()}`)

                needRestart = true
                this._outEventCall && this._outEventCall(`Update finished. ${event.getMessage()}`, event)

                break
            case jsb.EventAssetsManager.UPDATE_FAILED:
                // cc.log(`${cc.js.getClassName(this)} Update failed. ${event.getMessage()}`)

                this._updating = false
                this._canRetry = true
                this._outEventCall && this._outEventCall(`Update failed. ${event.getMessage()}`, event)

                break
            case jsb.EventAssetsManager.ERROR_UPDATING:
                // cc.log(`${cc.js.getClassName(this)} Asset update error: ${event.getAssetId()}, ${event.getMessage()}`)
                this._outEventCall && this._outEventCall(`Asset update error: ${event.getAssetId()}, ${event.getMessage()}`, event)

                break
            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                // cc.log(`${cc.js.getClassName(this)} ${event.getMessage()}`)
                this._outEventCall && this._outEventCall(`${event.getMessage()}`, event)

                break

            default:
                break
        }

        if (failed) {
            this._assetsManager.setEventCallback(null)
            this._updateListener = null
            this._updating = false
        }

        if (needRestart) {
            this._assetsManager.setEventCallback(null)
            this._updateListener = null
            // Prepend the manifest`s search path
            let searchPaths = jsb.fileUtils.getSearchPaths(),
            newPaths = this._assetsManager.getLocalManifest().getSearchPaths()
            cc.log(`${cc.js.getClassName(this)} newPaths = ${JSON.stringify(newPaths)}`)
            for (var i = 0; i < newPaths.length; i++) {
                if (searchPaths.indexOf(newPaths[i]) == -1) {
                    Array.prototype.unshift.apply(searchPaths, [newPaths[i]]);
                }
            }
            cc.log(`${cc.js.getClassName(this)} searchPaths = ${JSON.stringify(searchPaths)}`)
            // This value will be retrieved and appended to the default search path during game startup,
            // please refer to samples/js-tests/main.js for detailed usage.
            // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won`t take effect.
            cc.sys.localStorage.setItem(`HotUpdateSearchPaths`, JSON.stringify(searchPaths))
            jsb.fileUtils.setSearchPaths(searchPaths)

            cc.audioEngine.stopAll()
            cc.game.restart()
        }
    }

    //==============================================================================================================================
    //========================================================= 外部方法 ============================================================
    //==============================================================================================================================

    /**
     * 重试
     */
    retry(): void {
        cc.log(`${cc.js.getClassName(this)} retry`)

        if (!this._updating && this._canRetry) {
            this._canRetry = false

            cc.log(`${cc.js.getClassName(this)} Retry failed Assets...`)
            this._assetsManager.downloadFailedAssets()
        }
    }

    /**
     * 检测更新，检测结果会在_onEventCall中进行处理
     */
    checkUpdate(): void {
        cc.log(`${cc.js.getClassName(this)} checkUpdate`)

        if (this._updating) {
            cc.log(`${cc.js.getClassName(this)} Checking or updating ...`)
        } else {
            cc.log(`${cc.js.getClassName(this)} hotUpdate this._assetsManager.getState() = ${this._assetsManager.getState()}`)
            if (this._assetsManager.getState() === jsb.AssetsManager.State.UNINITED) {
                // Resolve md5 url
                let url = JKManager.OriginalManifestUrl
                if (cc.loader.md5Pipe) {
                    url = cc.loader.md5Pipe.transformURL(url)
                }
                cc.log(`${cc.js.getClassName(this)} hotUpdate url = ${url}`)
                this._assetsManager.loadLocalManifest(url)
            }
            if (!this._assetsManager.getLocalManifest() || !this._assetsManager.getLocalManifest().isLoaded()) {
                cc.log(`${cc.js.getClassName(this)} Failed to load local manifest ...`)
            } else {
                this._assetsManager.setEventCallback(this._onEventCall.bind(this))
                this._assetsManager.checkUpdate()
            }
        }
    }

    /**
     * 更新，更新中状态会在_onUpdateCall中进行处理
     */
    hotUpdate(): void {
        cc.log(`${cc.js.getClassName(this)} hotUpdate this._updating = ${this._updating}`)

        if (this._assetsManager && !this._updating) {
            this._assetsManager.setEventCallback(this._onUpdateCall.bind(this))

            cc.log(`${cc.js.getClassName(this)} hotUpdate this._assetsManager.getState() = ${this._assetsManager.getState()}`)
            if (this._assetsManager.getState() === jsb.AssetsManager.State.UNINITED) {
                // Resolve md5 url
                var url = JKManager.OriginalManifestUrl
                if (cc.loader.md5Pipe) {
                    url = cc.loader.md5Pipe.transformURL(url)
                }
                cc.log(`${cc.js.getClassName(this)} hotUpdate url = ${url}`)
                this._assetsManager.loadLocalManifest(url)
            }

            this._failCount = 0
            this._assetsManager.update()
            this._updating = true
        }
    }

    /**
     * 清理
     */
    clear(): void {
        if (this._updateListener) {
            this._assetsManager.setEventCallback(null)
            this._updateListener = null
        }
    }

    isUpdating(): boolean {
        return this._updating
    }
}